package com.im.project.service.impl;

import cn.dev33.satoken.secure.SaSecureUtil;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FastByteArrayOutputStream;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.code.kaptcha.Producer;
import com.im.common.config.MinioConfig;
import com.im.common.constant.Constant;
import com.im.common.enums.ResultEnum;
import com.im.common.enums.WsEnum;
import com.im.common.exception.BusinessException;
import com.im.common.util.GenerateIdUtil;
import com.im.common.util.StringTool;
import com.im.project.entity.*;
import com.im.project.entity.dto.*;
import com.im.project.entity.vo.*;
import com.im.project.mapper.*;
import com.im.project.rabbitmq.RabbitmqProducer;
import com.im.project.service.UserService;
import com.im.project.webSocket.UserChannelCtxMap;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.errors.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.*;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 用户 服务层接口实现
 *
 * @author yunZhouJiDi
 */
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Resource
    private UserMapper userMapper;

    @Resource
    private JavaMailSender javaMailSender;

    @Value("${spring.mail.username}")
    private String sender;

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private MinioClient minioClient;

    @Resource
    private MinioConfig minioConfig;

    @Value("${im.captchaType}")
    private String captchaType;

    @Resource(name = "captchaProducer")
    private Producer captchaProducer;

    @Resource(name = "captchaProducerMath")
    private Producer captchaProducerMath;

    @Resource
    private ContactMapper contactMapper;

    @Resource
    private ChatUserMapper chatUserMapper;

    @Resource
    private ClusterUserMapper clusterUserMapper;

    @Resource
    private ChatMessageMapper chatMessageMapper;

    @Resource
    private ContactApplyMapper contactApplyMapper;

    @Resource
    private RabbitmqProducer rabbitmqProducer;

    @Override
    public Boolean sendMailCode(SendMailCodeDto sendMailCodeDto) {
        String code = RandomUtil.randomNumbers(6);
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(sender);
        message.setTo(sendMailCodeDto.getEmail());
        message.setSubject(Constant.SystemConstant.SUBJECT);
        message.setText(String.format(Constant.SystemConstant.SUBJECT_CONTENT, code));
        javaMailSender.send(message);
        String verifyKey = Constant.RedisConstant.EMAIL_CODE + sendMailCodeDto.getEmail();
        redisTemplate.opsForValue().set(verifyKey, code, 5L, TimeUnit.MINUTES);
        log.info("发送给邮箱号：{}，的邮箱验证码为：{}", sendMailCodeDto.getEmail(), code);
        return true;
    }

    @Override
    @Transactional
    public Boolean register(RegisterDto registerDto) {
        String registerVerifyKey = Constant.RedisConstant.EMAIL_CODE + registerDto.getEmail();
        String emailCodeRedis = (String) redisTemplate.opsForValue().get(registerVerifyKey);
        if (null == emailCodeRedis) {
            throw new BusinessException(ResultEnum.CAPTCHA_LOSE_EFFICACY.getCode(), ResultEnum.CAPTCHA_LOSE_EFFICACY.getMessage());
        }
        if (!registerDto.getEmailCode().equalsIgnoreCase(emailCodeRedis)) {
            throw new BusinessException(ResultEnum.CAPTCHA_IS_ERROR.getCode(), ResultEnum.CAPTCHA_IS_ERROR.getMessage());
        }
        User userExist = userMapper.selectOne(new QueryWrapper<User>().eq("username", registerDto.getUsername()));
        if (!ObjectUtils.isEmpty(userExist)) {
            throw new BusinessException(ResultEnum.USERNAME_IS_EXIST.getCode(), ResultEnum.USERNAME_IS_EXIST.getMessage());
        }
        if (!registerDto.getPassword().equals(registerDto.getResetPassword())) {
            throw new BusinessException(ResultEnum.THE_RESET_PASSWORD_IS_INCORRECT.getCode(), ResultEnum.THE_RESET_PASSWORD_IS_INCORRECT.getMessage());
        }
        String icon = null;
        try {
            icon = generateIcon(registerDto.getUsername());
        } catch (IOException e) {
            e.printStackTrace();
        }
        String userId = GenerateIdUtil.generateUserId();
        userMapper.insert(
                new User()
                        .setUserId(userId)
                        .setEmail(registerDto.getEmail())
                        .setUsername(registerDto.getUsername())
                        .setIcon(icon)
                        .setPassword(SaSecureUtil.aesEncrypt(registerDto.getPassword(), registerDto.getEmail()))
                        .setCreateTime(new Date())
        );
        UserVo userVo = new UserVo().
                setUserId(userId).
                setUsername(registerDto.getUsername()).
                setEmail(registerDto.getEmail()).
                setIcon(icon).
                setSex(ResultEnum.MAN.getCode()).
                setNote(null).
                setJoinType(ResultEnum.AGREED_JOIN_TYPE.getCode()).
                setCreateTime(new Date()).
                setStatus(ResultEnum.ENABLE.getCode()).
                setAreaName(null);
        redisTemplate.opsForValue().set(Constant.RedisConstant.USER + userId, userVo);
        redisTemplate.delete(registerVerifyKey);
        return true;
    }

    private String generateIcon(String username) throws IOException {
        Color usernameColor = Color.CYAN;
        Color backgroundColor = Color.LIGHT_GRAY;
        BufferedImage image = new BufferedImage(200, 200, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = image.createGraphics();
        g2d.setColor(backgroundColor);
        g2d.fillRect(0, 0, 200, 200);
        String initials = username.substring(0, 1);
        Font font = null;
        if (StringTool.isChinese(initials)) {
            font = new Font("宋体", Font.BOLD, 100);
        } else {
            font = new Font("Arial", Font.BOLD, 100);
        }
        g2d.setFont(font);
        g2d.setColor(usernameColor);
        FontMetrics fontMetrics = g2d.getFontMetrics();
        Rectangle2D rect = fontMetrics.getStringBounds(initials, g2d);
        int centerX = (200 - (int) rect.getWidth()) / 2;
        int centerY = (200 - (int) rect.getHeight()) / 2 + fontMetrics.getAscent();
        g2d.drawString(initials, centerX, centerY);
        ByteArrayOutputStream baas = new ByteArrayOutputStream();
        ImageIO.write(image, "png", baas);
        byte[] imageBytes = baas.toByteArray();
        String objectName = UUID.randomUUID().toString() + ".png";
        try {
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(objectName)
                    .stream(new ByteArrayInputStream(imageBytes), imageBytes.length, -1)
                    .contentType("image/png")
                    .build());
        } catch (ErrorResponseException e) {
            e.printStackTrace();
        } catch (InsufficientDataException e) {
            e.printStackTrace();
        } catch (InternalException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidResponseException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (XmlParserException e) {
            e.printStackTrace();
        }
        return objectName;
    }

    @Override
    public CaptchaVo captcha(HttpServletResponse response) {
        String uuid = UUID.randomUUID().toString();
        String verifyKey = Constant.RedisConstant.CAPTCHA_CODE_KEY + uuid;
        String capStr = null, code = null;
        BufferedImage image = null;
        if ("math".equals(captchaType)) {
            String capText = captchaProducerMath.createText();
            capStr = capText.substring(0, capText.lastIndexOf("@"));
            code = capText.substring(capText.lastIndexOf("@") + 1);
            log.info("数组验证码：{}", code);
            image = captchaProducerMath.createImage(capStr);
        } else if ("char".equals(captchaType)) {
            capStr = code = captchaProducer.createText();
            log.info("字符验证码：{}", code);
            image = captchaProducer.createImage(capStr);
        }
        redisTemplate.opsForValue().set(verifyKey, code, 5L, TimeUnit.MINUTES);
        FastByteArrayOutputStream fastByteArrayOutputStream = new FastByteArrayOutputStream();
        try {
            ImageIO.write(image, "jpg", fastByteArrayOutputStream);
        } catch (IOException e) {
            log.error("验证码错误：{}", e.getMessage());
        }
        String str = "data:image/jpeg;base64,";
        return new CaptchaVo().
                setUuid(uuid).
                setImage(str + Base64.encode(fastByteArrayOutputStream.toByteArray()));
    }

    @Override
    public LoginVo login(LoginDto loginDto) {
        String captchaRedis = (String) redisTemplate.opsForValue().get(Constant.RedisConstant.CAPTCHA_CODE_KEY + loginDto.getUuid());
        if (null == captchaRedis) {
            throw new BusinessException(ResultEnum.CAPTCHA_LOSE_EFFICACY.getCode(), ResultEnum.CAPTCHA_LOSE_EFFICACY.getMessage());
        }
        if (!loginDto.getImage().equalsIgnoreCase(captchaRedis)) {
            throw new BusinessException(ResultEnum.CAPTCHA_IS_ERROR.getCode(), ResultEnum.CAPTCHA_IS_ERROR.getMessage());
        }
        User userExist = userMapper.selectOne(new QueryWrapper<User>().eq("email", loginDto.getEmail()));
        if (ObjectUtils.isEmpty(userExist)) {
            throw new BusinessException(ResultEnum.USERNAME_IS_NOT_EXIST.getCode(), ResultEnum.USERNAME_IS_NOT_EXIST.getMessage());
        }
        if (!userExist.getPassword().equals(SaSecureUtil.aesEncrypt(loginDto.getPassword(), userExist.getEmail()))) {
            long currentTime = System.currentTimeMillis();
            boolean flag = lockedUser(currentTime, userExist.getUsername());
            String str = "15";
            long timeStamp = System.currentTimeMillis() + 900000;
            if (redisTemplate.hasKey(Constant.RedisConstant.ERROR_COUNT + userExist.getUsername())) {
                int errorNum = Integer.parseInt(redisTemplate.opsForHash().get(Constant.RedisConstant.ERROR_COUNT + userExist.getUsername(), "errorNum").toString());
                if (flag && errorNum == Constant.SystemConstant.ERROR_COUNT) {
                    redisTemplate.opsForHash().put(Constant.RedisConstant.ERROR_COUNT + userExist.getUsername(), "errorNum", 1);
                } else {
                    redisTemplate.opsForHash().increment(Constant.RedisConstant.ERROR_COUNT + userExist.getUsername(), "errorNum", 1);
                }
                redisTemplate.opsForHash().put(Constant.RedisConstant.ERROR_COUNT + userExist.getUsername(), "loginTime", timeStamp);
            } else {
                Map<String, Object> map = new HashMap<>();
                map.put("errorNum", 1);
                map.put("loginTime", timeStamp);
                redisTemplate.opsForHash().putAll(Constant.RedisConstant.ERROR_COUNT + userExist.getUsername(), map);
            }
            int i = Integer.parseInt(redisTemplate.opsForHash().get(Constant.RedisConstant.ERROR_COUNT + userExist.getUsername(), "errorNum").toString());
            if (i == Constant.SystemConstant.ERROR_COUNT) {
                throw new BusinessException(ResultEnum.LOCKED_USER.getCode(), String.format(Constant.SystemConstant.USER_LOCK_CONTENT, Constant.SystemConstant.ERROR_COUNT, str));
            }
            throw new BusinessException(ResultEnum.LOCKED_USER.getCode(), String.format(Constant.SystemConstant.USER_REST_CONTENT, Constant.SystemConstant.ERROR_COUNT, Constant.SystemConstant.ERROR_COUNT - i));
        } else {
            UserVo userVo = new UserVo().
                    setUserId(userExist.getUserId()).
                    setUsername(userExist.getUsername()).
                    setEmail(userExist.getEmail()).
                    setIcon(userExist.getIcon()).
                    setSex(userExist.getSex()).
                    setNote(userExist.getNote()).
                    setJoinType(userExist.getJoinType()).
                    setCreateTime(userExist.getCreateTime()).
                    setStatus(userExist.getStatus()).
                    setAreaName(userExist.getAreaName());
            StpUtil.login(userExist.getUserId());
            String token = StpUtil.getTokenInfo().getTokenValue();
            redisTemplate.delete(Constant.RedisConstant.CAPTCHA_CODE_KEY + loginDto.getUuid());
            redisTemplate.delete(Constant.RedisConstant.ERROR_COUNT + userExist.getUsername());
            redisTemplate.delete(Constant.RedisConstant.USER_CONTACT + userVo.getUserId());
            List<String> contactIdList = contactMapper.selectList(new QueryWrapper<Contact>().
                    eq("user_id", userVo.getUserId()).
                    eq("status", ResultEnum.FRIEND_CONTACT_STATUS.getCode())).
                    stream().
                    map(item -> item.getContactId()).
                    collect(Collectors.toList());
            if (contactIdList.size() > 0) {
                redisTemplate.opsForList().leftPushAll(Constant.RedisConstant.USER_CONTACT + userVo.getUserId(), contactIdList);
            }
            return new LoginVo().setUserVo(userVo).setToken(token);
        }
    }

    private boolean lockedUser(long currentTime, String username) {
        boolean flag = false;
        if (redisTemplate.hasKey(Constant.RedisConstant.ERROR_COUNT + username)) {
            long loginTime = Long.parseLong(redisTemplate.opsForHash().get(Constant.RedisConstant.ERROR_COUNT + username, "loginTime").toString());
            int errorNum = Integer.parseInt(redisTemplate.opsForHash().get(Constant.RedisConstant.ERROR_COUNT + username, "errorNum").toString());
            if (errorNum >= Constant.SystemConstant.ERROR_COUNT && currentTime < loginTime) {
                Duration between = LocalDateTimeUtil.between(LocalDateTimeUtil.of(currentTime), LocalDateTimeUtil.of(loginTime));
                throw new BusinessException(ResultEnum.LOCKED_USER.getCode(), String.format(Constant.SystemConstant.USER_LOCK_REST_CONTENT, between.toMinutes()));
            } else {
                flag = true;
            }
        }
        return flag;
    }

    @Override
    public Boolean resetPassword(ResetPasswordDto resetPasswordDto) {
        String resetPasswordVerifyKey = Constant.RedisConstant.EMAIL_CODE + resetPasswordDto.getEmail();
        String emailCodeRedis = (String) redisTemplate.opsForValue().get(resetPasswordVerifyKey);
        if (null == emailCodeRedis) {
            throw new BusinessException(ResultEnum.CAPTCHA_LOSE_EFFICACY.getCode(), ResultEnum.CAPTCHA_LOSE_EFFICACY.getMessage());
        }
        if (!resetPasswordDto.getEmailCode().equalsIgnoreCase(emailCodeRedis)) {
            throw new BusinessException(ResultEnum.CAPTCHA_IS_ERROR.getCode(), ResultEnum.CAPTCHA_IS_ERROR.getMessage());
        }
        User userExist = userMapper.selectOne(new QueryWrapper<User>().eq("username", resetPasswordDto.getUsername()));
        if (ObjectUtils.isEmpty(userExist)) {
            throw new BusinessException(ResultEnum.USERNAME_IS_NOT_EXIST.getCode(), ResultEnum.USERNAME_IS_NOT_EXIST.getMessage());
        }
        if (!resetPasswordDto.getNewPassword().equals(resetPasswordDto.getResetPassword())) {
            throw new BusinessException(ResultEnum.THE_NEW_PASSWORD_IS_NOT_THE_SAME_AS_THE_REPEATED_PASSWORD.getCode(), ResultEnum.THE_NEW_PASSWORD_IS_NOT_THE_SAME_AS_THE_REPEATED_PASSWORD.getMessage());
        }
        userExist.setPassword(SaSecureUtil.aesEncrypt(resetPasswordDto.getNewPassword(), userExist.getEmail()));
        userMapper.updateById(userExist);
        redisTemplate.delete(resetPasswordVerifyKey);
        return true;
    }

    @Override
    public Boolean logout() {
        UserChannelCtxMap.closeContext(StpUtil.getLoginIdAsString());
        StpUtil.logout();
        return true;
    }

    @Override
    public UserVo queryCurrentUserDetail() {
        UserVo userVo = (UserVo) redisTemplate.opsForValue().get(Constant.RedisConstant.USER + StpUtil.getLoginIdAsString());
        return userVo;
    }

    @Override
    @Transactional
    public UserVo updateCurrentUser(UpdateUserDto updateUserDto) {
        String currentUserId = StpUtil.getLoginIdAsString();
        userMapper.update(
                new User().
                        setUsername(updateUserDto.getUsername()).
                        setIcon(updateUserDto.getIcon()).
                        setSex(updateUserDto.getSex()).
                        setNote(updateUserDto.getNote()).
                        setJoinType(updateUserDto.getJoinType()).
                        setAreaName(updateUserDto.getAreaName()),
                new QueryWrapper<User>().eq("user_id", currentUserId)
        );
        User user = userMapper.selectOne(new QueryWrapper<User>().eq("user_id", currentUserId));
        UserVo userVo = new UserVo().
                setUserId(user.getUserId()).
                setUsername(user.getUsername()).
                setEmail(user.getEmail()).
                setIcon(user.getIcon()).
                setSex(user.getSex()).
                setNote(user.getNote()).
                setJoinType(user.getJoinType()).
                setCreateTime(user.getCreateTime()).
                setStatus(user.getStatus()).
                setAreaName(user.getAreaName());
        UserVo beforeUserVo = (UserVo) redisTemplate.opsForValue().get(Constant.RedisConstant.USER + StpUtil.getLoginIdAsString());
        redisTemplate.delete(Constant.RedisConstant.USER + StpUtil.getLoginIdAsString());
        redisTemplate.opsForValue().set(Constant.RedisConstant.USER + userVo.getUserId(), userVo);
        if (!updateUserDto.getUsername().equalsIgnoreCase(beforeUserVo.getUsername()) || !updateUserDto.getIcon().equalsIgnoreCase(beforeUserVo.getIcon())) {
            List<String> contactList = contactMapper.selectList(new QueryWrapper<Contact>().
                    eq("user_id", currentUserId).
                    eq("status", ResultEnum.FRIEND_CONTACT_STATUS.getCode())
            ).stream().map(item -> item.getContactId()).collect(Collectors.toList());
            contactList.forEach(contactId -> {
                rabbitmqProducer.sendMessageProducer(
                        Constant.RabbitmqConstant.ROUTING_KEY + Constant.RabbitmqConstant.SEND_MESSAGE,
                        new MessageVo<UpdateMessageVo>().
                                setCommandCode(WsEnum.CONTACT_UPDATE.getCode()).
                                setCommandName(WsEnum.CONTACT_UPDATE.getMessage()).
                                setContactId(contactId).
                                setData(
                                        new UpdateMessageVo().
                                                setUserId(contactId).
                                                setName(updateUserDto.getUsername()).
                                                setIcon(updateUserDto.getIcon()).
                                                setContactId(currentUserId)
                                )
                );
            });
            chatUserMapper.update(
                    new ChatUser().setContactName(updateUserDto.getUsername()),
                    new QueryWrapper<ChatUser>().eq("contact_id", currentUserId)
            );
        }
        return userVo;
    }

    @Override
    public Boolean updateUserPassword(UpdateUserPasswordDto updateUserPasswordDto) {
        String currentUserId = StpUtil.getLoginIdAsString();
        User user = userMapper.selectOne(new QueryWrapper<User>().eq("user_id", currentUserId));
        user.setPassword(SaSecureUtil.aesEncrypt(updateUserPasswordDto.getPassword(), user.getEmail()));
        userMapper.updateById(user);
        return true;
    }

    @Override
    public void pullMessage(String userId) {
        userMapper.update(new User().setLastLoginTime(new Date()), new QueryWrapper<User>().eq("user_id", userId));
        User user = userMapper.selectOne(new QueryWrapper<User>().eq("user_id", userId));
        Long sourceLastOffTime = user.getLastOffTime();
        Long lastOffTime = sourceLastOffTime;
        if (sourceLastOffTime != null && System.currentTimeMillis() - Constant.SystemConstant.MILLISECOND_SEVEN_DAYS_AGO > sourceLastOffTime) {
            lastOffTime = Constant.SystemConstant.MILLISECOND_SEVEN_DAYS_AGO;
        }
        List<ChatUserVo> chatUserList = chatUserMapper.queryChatUserList(userId);
        List<ClusterUser> clusterUserList = clusterUserMapper.selectList(new QueryWrapper<ClusterUser>().eq("user_id", userId));
        List<String> contactIdList = new ArrayList<>();
        if (clusterUserList.size() > 0) {
            contactIdList = clusterUserList.stream().map(item -> item.getClusterId()).collect(Collectors.toList());
        }
        contactIdList.add(userId);
        List<ChatMessage> chatMessageList = chatMessageMapper.queryChatMessageList(lastOffTime, contactIdList);
        Integer count = contactApplyMapper.selectCount(
                new QueryWrapper<ContactApply>().
                        eq("receive_user_id", userId).
                        ge("last_apply_time", sourceLastOffTime).
                        eq("status", ResultEnum.PEND_CONTACT_APPLY_STATUS.getCode())
        );
        List<String> contactReadList = (List<String>) redisTemplate.opsForList().range(Constant.RedisConstant.READ_CHAT_MESSAGE + userId, 0, -1);
        if (contactReadList == null) {
            contactReadList = new ArrayList<String>();
        }
        List<String> chatIdList = (List<String>) redisTemplate.opsForList().range(Constant.RedisConstant.DELETE_CHAT + userId, 0, -1);
        if (chatIdList == null) {
            chatIdList = new ArrayList<String>();
        }
        List<String> clearChatIdList = (List<String>) redisTemplate.opsForList().range(Constant.RedisConstant.CLEAR_CHAT + userId, 0, -1);
        if (clearChatIdList == null) {
            clearChatIdList = new ArrayList<String>();
        }
        rabbitmqProducer.sendMessageProducer(
                Constant.RabbitmqConstant.ROUTING_KEY + Constant.RabbitmqConstant.SEND_MESSAGE,
                new MessageVo<WsInitDataVo>().
                        setCommandCode(WsEnum.INIT.getCode()).
                        setCommandName(WsEnum.INIT.getMessage()).
                        setContactId(userId).
                        setData(
                                new WsInitDataVo().
                                        setChatUserList(chatUserList).
                                        setChatMessageList(chatMessageList).
                                        setApplyCount(count).
                                        setContactReadList(contactReadList) .
                                        setChatIdList(chatIdList).
                                        setClearChatIdList(clearChatIdList)
                        )
        );
        redisTemplate.delete(Constant.RedisConstant.READ_CHAT_MESSAGE + userId);
        redisTemplate.delete(Constant.RedisConstant.DELETE_CHAT + userId);
        redisTemplate.delete(Constant.RedisConstant.CLEAR_CHAT + userId);
    }
}
